package com.googlecode.framework.orm.hibernate;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.LockOptions;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.transform.ResultTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

import com.googlecode.framework.core.pagination.OrderablePagination;
import com.googlecode.framework.dao.Persistable;
import com.googlecode.framework.orm.hibernate.criterion.SafeDetachedCriteria;


public class DefaultHibernateOrmExecutor implements HibernateOrmExecutor {
    
    private Logger logger = LoggerFactory.getLogger(getClass());

    private SessionFactory sessionFactory;
    
    @Override
    public <E extends Persistable> E get(Class<E> clazz, Serializable id) throws DataAccessException {
        return get(clazz, id, null, null);
    }

    @Override
    public <E extends Persistable> E get(Class<E> clazz, Serializable id, LockOptions lockOptions) throws DataAccessException {
        return get(clazz, id, lockOptions, null);
    }

    @Override
    public <E extends Persistable> E get(Class<E> clazz, Serializable id, String[] initFields) throws DataAccessException {
        return get(clazz, id, null, initFields);
    }

    @Override
    public <E extends Persistable> E get(final Class<E> clazz, final Serializable id, final LockOptions lockOptions,
            final String[] initFields) throws DataAccessException {
        return doExecute(new HibernateCallback<E>() {
            @Override
            public E onExecute(Session session) {
                Object o;
                if (lockOptions == null) {
                    o = getSession().get(clazz, id);
                } else {
                    o = getSession().get(clazz, id, lockOptions);
                }
                if (o != null && !ObjectUtils.isEmpty(initFields)) {
                    initProperties(o, initFields);
                }
                return clazz.cast(o);
            }
        });
    }
    
    @Override
    public Serializable save(final Persistable entity) throws DataAccessException {
        return doExecute(new HibernateCallback<Serializable>() {
            @Override
            public Serializable onExecute(Session session) {
                return session.save(entity);
            }
        });
    }

    @Override
    public Serializable[] save(final Persistable[] entities) throws DataAccessException {
        if (ObjectUtils.isEmpty(entities)) {
            return null;
        }
        return doExecute(new HibernateCallback<Serializable[]>() {
            @Override
            public Serializable[] onExecute(Session session) {
                Collection<Serializable> ids = new ArrayList<Serializable>();
                for (Persistable entity : entities) {
                    ids.add(session.save(entity));
                }
                return ids.toArray(new Serializable[ids.size()]);
            }
        });
    }

    @Override
    public Collection<Serializable> save(final Collection<Persistable> entities) throws DataAccessException {
        if (CollectionUtils.isEmpty(entities)) {
            return null;
        }
        return doExecute(new HibernateCallback<Collection<Serializable>>() {
            @Override
            public Collection<Serializable> onExecute(Session session) {
                Collection<Serializable> ids = new ArrayList<Serializable>();
                for (Iterator<Persistable> it = entities.iterator(); it.hasNext(); ) {
                    ids.add(session.save(it.next()));
                }
                return ids;
            }
        });
    }

    @Override
    public void update(final Persistable entity) throws DataAccessException {
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                session.update(entity);
                return null;
            }
        });
    }

    @Override
    public void update(final Persistable[] entities) throws DataAccessException {
        if (ObjectUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.update(entity);
                }
                return null;
            }
        });        
    }

    @Override
    public void update(final Collection<Persistable> entities) throws DataAccessException {
        if (CollectionUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.update(entity);
                }
                return null;
            }
        });        
    }

    @Override
    public void saveOrUpdate(final Persistable entity) throws DataAccessException {
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                session.saveOrUpdate(entity);
                return null;
            }
        });          
    }

    @Override
    public void saveOrUpdate(final Persistable[] entities) throws DataAccessException {
        if (ObjectUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.saveOrUpdate(entity);
                }
                return null;
            }
        });  
    }

    @Override
    public void saveOrUpdate(final Collection<Persistable> entities) throws DataAccessException {
        if (CollectionUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.saveOrUpdate(entity);
                }
                return null;
            }
        });          
    }

    @Override
    public void delete(Class<? extends Persistable> clazz, Serializable id) throws DataAccessException {
        delete(get(clazz, id));
    }
    
    @Override
    public void delete(final Persistable entity) throws DataAccessException {
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                session.delete(entity);
                return null;
            }
        });          
    }

    @Override
    public void delete(final Persistable[] entities) throws DataAccessException {
        if (ObjectUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.delete(entity);
                }
                return null;
            }
        });            
    }

    @Override
    public void delete(final Collection<Persistable> entities) throws DataAccessException {
        if (CollectionUtils.isEmpty(entities)) {
            return;
        }
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                for (Persistable entity : entities) {
                    session.delete(entity);
                }
                return null;
            }
        });        
    }

    @Override
    public <E extends Persistable> List<E> executeForObjectList(final SafeDetachedCriteria criteria) {
        return executeForObjectList(criteria, (OrderablePagination) null, (String[]) null);
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectList(final SafeDetachedCriteria criteria, final OrderablePagination op) {
        return executeForObjectList(criteria, op, (String[]) null);
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectList(final SafeDetachedCriteria criteria, final OrderablePagination op, final String[] initFields) {
        return doExecute(new HibernateCallback<List<E>>() {
            @SuppressWarnings("unchecked")
            @Override
            public List<E> onExecute(Session session) {
                Criteria executableCriteria = criteria.getExecutableCriteria(session);
                prepareCriteria(executableCriteria);
                
                // 如果设置了分页，则统计总数&设置分页条件
                // 注意：不支持有group by语句的总数统计。当有group by或order by语句时，统计总数时可能会出现sql语法错误
                if (op != null) {
                    // 保存原先的投影
                    Projection orginalProjection = ((CriteriaImpl)executableCriteria).getProjection();
                    ResultTransformer orginalResultTransformer = ((CriteriaImpl)executableCriteria).getResultTransformer();
                    // 设置新的投影，取得总记录数
                    Number items = (Number) executableCriteria.setProjection(Projections.rowCount()).uniqueResult();
                    if (items != null) {
                        op.setItems(items.intValue());
                    }
                    // 还原原先的投影
                    executableCriteria.setProjection(orginalProjection);
                    executableCriteria.setResultTransformer(orginalResultTransformer);
                    
                    // 设置分页条件
                    executableCriteria.setFirstResult(op.getOffset());
                    executableCriteria.setMaxResults(op.getLength());
                    // 设置排序条件
                    com.googlecode.framework.core.pagination.Order[] orders = op.getOrders();
                    if (!ObjectUtils.isEmpty(orders)) {
                        for (com.googlecode.framework.core.pagination.Order order : orders) {
                            if (order.isAscending()) {
                                executableCriteria.addOrder(Order.asc(order.name()));
                            } else {
                                executableCriteria.addOrder(Order.desc(order.name()));
                            }
                        }
                    }
                }
                
                // 获取数据
                List<E> result = executableCriteria.list();
                if (!ObjectUtils.isEmpty(initFields)) {
                    initProperties(result, initFields);    
                }
                return result;
            }
        });
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectList(final SafeDetachedCriteria criteria, final int offset, final int length) {
        return executeForObjectList(criteria, offset, length, null);
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectList(final SafeDetachedCriteria criteria, final int offset, final int length, final String[] initFields) {
        return doExecute(new HibernateCallback<List<E>>() {
            @SuppressWarnings("unchecked")
            @Override
            public List<E> onExecute(Session session) {
                Criteria executableCriteria = criteria.getExecutableCriteria(session);
                prepareCriteria(executableCriteria);
                
                executableCriteria.setFirstResult(offset);
                executableCriteria.setMaxResults(length);
                
                // 获取数据
                List<E> result = executableCriteria.list();
                if (!ObjectUtils.isEmpty(initFields)) {
                    initProperties(result, initFields);    
                }
                return result;
            }
        });
    }
    
    @Override
    public <E extends Persistable> E executeForObject(final SafeDetachedCriteria criteria) {
        return executeForObject(criteria, null);
    }
    
    @Override
    public <E extends Persistable> E executeForObject(final SafeDetachedCriteria criteria, final String[] initFields) {
        return doExecute(new HibernateCallback<E>() {
            @SuppressWarnings("unchecked")
            @Override
            public E onExecute(Session session) {
                Criteria executableCriteria = criteria.getExecutableCriteria(session);
                prepareCriteria(executableCriteria);
                
                executableCriteria.setFirstResult(0);
                executableCriteria.setMaxResults(2);
                List<E> result = executableCriteria.list();
                if (result == null || result.isEmpty()) {
                    return null;
                } else if (result.size() > 1) {
                    logger.warn("find too many results.");
                }
                
                E e = result.get(0);
                if (!ObjectUtils.isEmpty(initFields)) {
                    initProperties(e, initFields);    
                }
                return e;
            }
        });
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectListByHql(final String hql, final Object[] params) {
        return executeForObjectListByHql(hql, params, null, null);
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectListByHql(final String hql, final Object[] params, final OrderablePagination op) {
        return executeForObjectListByHql(hql, params, op, null);
    }
    
    @Override
    public <E extends Persistable> List<E> executeForObjectListByHql(final String hql, final Object[] params,
            final OrderablePagination op, final String[] initFields) {
        return doExecute(new HibernateCallback<List<E>>() {
            @SuppressWarnings("unchecked")
            @Override
            public List<E> onExecute(Session session) {
                
                Query query = session.createQuery(hql);
                prepareQuery(query);
                applyParameters(query, params);
                
                // 如果设置了分页，则统计总数&设置分页条件
                // 注意：不支持有group by语句的总数统计。当有group by或order by语句时，统计总数时可能会出现sql语法错误
                if (op != null) {
                    // TODO 根据原先的hql语句，生成统计总数的sql语句
                    String sql = null;
                    Query countSQLQuery = session.createSQLQuery(sql);
                    applyParameters(countSQLQuery, params);
                    
                    // 取得统计总数
                    List<?> l = countSQLQuery.list();
                    Number items = ((Number)l.get(0)).intValue();
                    if (items != null) {
                        op.setItems(items.intValue());
                    }
                    
                    // 设置分页条件
                    query.setFirstResult(op.getOffset());
                    query.setMaxResults(op.getLength());
                    // TODO 设置排序条件
                    com.googlecode.framework.core.pagination.Order[] orders = op.getOrders();
                    if (!ObjectUtils.isEmpty(orders)) {
                        for (com.googlecode.framework.core.pagination.Order order : orders) {
                            if (order.isAscending()) {
                            } else {
                            }
                        }
                    }
                }
                
                // 获取数据
                List<E> result = query.list();
                if (!ObjectUtils.isEmpty(initFields)) {
                    initProperties(result, initFields);    
                }
                return result;
            }
        });
    }
    
    @Override
    public <E extends Persistable> E executeForObjectByHql(final String hql, final Object[] params) {
        return executeForObjectByHql(hql, params, null);
    }
    
    @Override
    public <E extends Persistable> E executeForObjectByHql(final String hql, final Object[] params, final String[] initFields) {
        return doExecute(new HibernateCallback<E>() {
            @SuppressWarnings("unchecked")
            @Override
            public E onExecute(Session session) {
                
                Query query = session.createQuery(hql);
                prepareQuery(query);
                applyParameters(query, params);
                
                query.setFirstResult(0);
                query.setMaxResults(2);
                // 获取数据
                List<E> result = query.list();
                if (result == null || result.isEmpty()) {
                    return null;
                } else if (result.size() > 1) {
                    logger.warn("find too many results.");
                }
                
                E e = result.get(0);
                if (!ObjectUtils.isEmpty(initFields)) {
                    initProperties(e, initFields);    
                }
                return e;
            }
        });
    }
    
    @Override
    public int bulkUpdate(final String strQuery) throws DataAccessException {
        return bulkUpdate(strQuery, (Object[]) null);
    }

    @Override
    public int bulkUpdate(final String strQuery, final Object... values) throws DataAccessException {
        return doExecute(new HibernateCallback<Integer>() {
            @Override
            public Integer onExecute(Session session) {
                Query query = session.createQuery(strQuery);
                prepareQuery(query);
                if (values != null) {
                    for (int i = 0; i < values.length; i++) {
                        query.setParameter(i, values[i]);
                    }
                }
                return query.executeUpdate();                
            }
        });
    }
    
    @Override
    public void flush() throws DataAccessException {
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                session.flush();
                return null;
            }
        });
    }

    @Override
    public void clear() throws DataAccessException {
        doExecute(new HibernateCallback<Object>() {
            @Override
            public Object onExecute(Session session) {
                session.clear();
                return null;
            }
        });     
    }
    
    protected Session getSession() {
        return sessionFactory.getCurrentSession();
    }
    
    protected void initProperties(Object o, String[] initFields) {
        for (String fieldName : initFields) {
            String[] splitFieldNames = fieldName.split("\\.");
            Object targetObject = o;
            for (String splitFieldName : splitFieldNames) {
                if (targetObject != null) {
                    if (targetObject instanceof Collection<?>) {
                        for (Iterator<?> it = ((Collection<?>) targetObject).iterator(); it.hasNext();) {
                            Object proxyObject = getProxyObject(it.next(), splitFieldName);
                            Hibernate.initialize(proxyObject);
                        }
                        break;
                    } else {
                        Object proxyObject = getProxyObject(targetObject, splitFieldName);
                        Hibernate.initialize(proxyObject);
                        targetObject = proxyObject;
                    }
                }
            }
        }
    }
    
    private Object getProxyObject(Object o, String propertyName) {
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(o);
        return bw.getPropertyValue(propertyName);
    }
    
    public <T> T doExecute(HibernateCallback<T> action) throws DataAccessException {
        return action.onExecute(getSession());
    }
    
    protected void prepareQuery(Query query) {
    }
    protected void prepareCriteria(Criteria criteria) {
    }
    
    protected void applyParameters(Query q, Object[] params) {
        if (!ObjectUtils.isEmpty(params)) {
            for (int i = 0; i < params.length; i++) {
                q.setParameter(i, params[i]);
            }
        }
    }
    
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    protected interface HibernateCallback<T> {
        T onExecute(Session session);
    }
}
